package protocol.xmpp;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.RosterGroup;
import org.jivesoftware.smack.RosterListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.OrFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.packet.VCard;

import protocol.domain.GroupEntry;
import protocol.domain.InstantMessage;
import protocol.domain.Status;
import protocol.domain.UserPresence;
import protocol.generic.AbstractProtocolManager;
import protocol.generic.Group;
import protocol.generic.ProtocolException;

/**
 * Implementation of XMPP protocol manager
 * 
 * @author hAmmeRoL
 * 
 */
public class XMPPProtocolManager extends AbstractProtocolManager implements
		PacketListener, RosterListener {

	// XMPP connection
	private XMPPConnection conn;

	// filter the Message class and presence class
	private PacketFilter filter = new OrFilter(new PacketTypeFilter(
			Message.class), new PacketTypeFilter(Presence.class));

	// XMPP user name DOES NOT contain domain name. e.g. hAmmeRoL
	private String userName;

	// domain name of XMPP protocol. e.g. gmail.com
	private String domainName;

	// hashmap containing all the chat objects
	private HashMap<String, Chat> chatMap;

	// VCard for XMPP protocol
	private VCard vCard;

	// presence
	private Presence presence;
	
	// block list
	private ArrayList<String> blockList;

	/**
	 * Construct a new XMPPProtocolManager
	 * 
	 * @param protocol
	 *            the protocol name
	 * @param userName
	 *            the user id
	 * @param password
	 *            the password
	 * @param domainName
	 *            the domain name
	 * @param hostName
	 *            the host name
	 * @param port
	 *            the port of the host
	 */
	public XMPPProtocolManager(String protocol, String userName,
			String password, String domainName, String hostName, int port) {
		this.protocol = protocol;
		this.userName = userName;
		this.password = password;
		this.domainName = domainName;
		// hAmmeRoL + @ + gmail.com
		this.userID = userName + "@" + domainName;
		this.hostName = hostName;
		this.port = port;
		chatMap = new HashMap<String, Chat>();
		this.vCard = new VCard();
		this.presence = new Presence(Presence.Type.available);
		blockList = new ArrayList<String>();

		// set roster subscription mode
		Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);

		// XMPPConnection.DEBUG_ENABLED = true;

	}

	public void connect() throws ProtocolException {
		ConnectionConfiguration config = new ConnectionConfiguration(hostName,
				port, domainName);
		conn = new XMPPConnection(config);
		try {
			conn.connect();
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
	}

	public void login() throws ProtocolException {
		try {
			// login
			conn.login(userName, password, "Yaim", true);

			// make sure the main loop processes logged in signal first
			eventHandler.loggedIn();

			// add listener to this until now
			conn.getRoster().addRosterListener(this);
			conn.addPacketListener(this, filter);
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
	}

	public void close() {
		conn.disconnect();
	}

	public void sendMessage(InstantMessage message) throws ProtocolException {
		Chat chat;

		// if already have chat object
		if (chatMap.containsKey(message.getDestinationID())) {
			chat = chatMap.get(message.getDestinationID());
		} else {
			// construct a new one
			chat = conn.getChatManager().createChat(message.getDestinationID(),
					null);
			chatMap.put(message.getDestinationID(), chat);
		}
		try {
			chat.sendMessage(message.getContent());
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
	}

	/**
	 * Process incoming packet
	 */
	public void processPacket(Packet packet) {

		if (packet instanceof Message) {
			// now process only message packet
			Message message = (Message) packet;

			String content = message.getBody();

			String id = message.getFrom();

			// get the actual id
			int index = id.indexOf("/");
			String from = id.substring(0, index);

			InstantMessage temp = new InstantMessage(from, userID, content);

			eventHandler.messageArrived(temp);
		} else if (packet instanceof Presence) {
			// process presence
			Presence presence = (Presence) packet;
			System.out.println("Presence packet : " + presence.getFrom() + ", "
					+ presence.getMode() + ", " + presence.getType());
			if (presence.getType() == Presence.Type.subscribe) {
				eventHandler.authenticationPending(presence.getFrom(), this);
			}
		}
	}

	@Override
	public ArrayList<Group> getGroups() {
		// get all entries
		Roster roster = conn.getRoster();
		Collection<RosterEntry> entries = roster.getEntries();

		if (this.DEBUG_ENABLED) {
			System.out.println("Initial roster size : " + entries.size());
		}

		HashMap<String, Group> groups = new HashMap<String, Group>();
		for (RosterEntry entry : entries) {

			Collection<RosterGroup> newGroups = entry.getGroups();
			Presence presence = roster.getPresence(entry.getUser());
			Status status;

			// get status
			if (presence.getType() == Presence.Type.unavailable) {
				status = Status.UNAVAILABLE;
			} else {
				status = Status.AVAILABLE;
			}
			UserPresence newPresence = new UserPresence(entry.getUser(), status);

			boolean isBlocked = false;

			if (this.DEBUG_ENABLED) {
				System.out.println("name : " + entry.getName() + ", user: "
						+ entry.getUser() + ", pending: " + entry.getStatus()
						+ ", type: " + entry.getType() + ", blocked: "
						+ isBlocked);
			}

			// entry in unfiled group
			if (newGroups.size() == 0) {
				// unfiled group
				Group unfiled = new Group();
				if (this.DEBUG_ENABLED) {
					System.out.println("Group: " + unfiled.getName());
				}
				if (groups.containsKey(unfiled.getName())) {
					Group group = groups.get(unfiled.getName());
					GroupEntry groupEntry = new GroupEntry(entry.getName(),
							entry.getUser(), group.getName(), newPresence,
							isBlocked);
					group.addGroupEntry(groupEntry);
				} else {
					Group group = new Group(unfiled.getName());
					GroupEntry groupEntry = new GroupEntry(entry.getName(),
							entry.getUser(), group.getName(), newPresence,
							isBlocked);
					group.addGroupEntry(groupEntry);
					groups.put(group.getName(), group);

				}
			} else {
				for (RosterGroup newGroup : newGroups) {
					if (this.DEBUG_ENABLED) {
						System.out.println("Group: " + newGroup.getName());
					}
					if (groups.containsKey(newGroup.getName())) {
						Group group = groups.get(newGroup.getName());
						GroupEntry groupEntry = new GroupEntry(entry.getName(),
								entry.getUser(), group.getName(), newPresence,
								isBlocked);
						group.addGroupEntry(groupEntry);
					} else {
						Group group = new Group(newGroup.getName());
						GroupEntry groupEntry = new GroupEntry(entry.getName(),
								entry.getUser(), group.getName(), newPresence,
								isBlocked);
						group.addGroupEntry(groupEntry);
						groups.put(group.getName(), group);

					}
				}
			}
		}
		return new ArrayList<Group>(groups.values());

	}

	@Override
	public byte[] getAvatar(String clientID) throws ProtocolException {

		try {
			vCard.load(conn, clientID); // load someone's VCard
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
		return vCard.getAvatar();

	}

	@Override
	public void setAvatar(String fileName) throws ProtocolException {
		try {
			vCard.setAvatar(new URL("file:///" + fileName));
			vCard.save(conn);
		} catch (Exception e) {
			throw new ProtocolException(e.toString());
		}
	}

	@Override
	public void createGroup(String groupName) {
		conn.getRoster().createGroup(groupName);
	}

	@Override
	public void deleteGroup(String groupName) throws ProtocolException {
		Roster roster = conn.getRoster();
		RosterGroup group = roster.getGroup(groupName);
		ArrayList<RosterEntry> entries = (ArrayList<RosterEntry>) group
				.getEntries();
		for (RosterEntry entry : entries) {
			try {
				group.removeEntry(entry);
			} catch (XMPPException e) {
				throw new ProtocolException(e.toString());
			}
		}
	}

	@Override
	public void renameGroup(String oldName, String newName) {
		Roster roster = conn.getRoster();
		RosterGroup group = roster.getGroup(oldName);
		group.setName(newName);
	}

	@Override
	public void moveEntryToGroup(String oldGroupName, String clientID,
			String newGroupName) throws ProtocolException {
		Roster roster = conn.getRoster();
		for (RosterGroup g : roster.getGroups()) {
			System.out.println("every group	" + g.getName());
		}
		RosterEntry entry;
		if (oldGroupName.equalsIgnoreCase("Unfiled Group")) {
			System.out.println("Unfiled Group");
			entry = roster.getEntry(clientID);
			RosterGroup newGroup = roster.getGroup(newGroupName);
			System.out.println("new Group name	" + newGroup.getName());
			try {
				//roster.removeEntry(entry);
				newGroup.addEntry(entry);
			} catch (XMPPException e) {
				e.printStackTrace();
			}
		} else {
			System.out.println("User Defined Group");
			RosterGroup oldGroup = roster.getGroup(oldGroupName);
			RosterGroup newGroup = roster.getGroup(newGroupName);
			entry = oldGroup.getEntry(clientID);
			System.out.println("new Group name	" + newGroup.getName());
			System.out.println("old Group name	" + oldGroup.getName());
			try {
				newGroup.addEntry(entry);
				oldGroup.removeEntry(entry);
			} catch (XMPPException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	public void copyEntryToGroup(String clientID, String newGroupName)
			throws ProtocolException {
		Roster roster = conn.getRoster();
		RosterGroup group = roster.getGroup(newGroupName);

		RosterEntry entry = roster.getEntry(clientID);
		try {
			group.addEntry(entry);
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
	}

	@Override
	public void deleteEntryFromGroup(String groupName, String clientID)
			throws ProtocolException {
		Roster roster = conn.getRoster();
		if (groupName.equalsIgnoreCase("Unfiled Group")) {
			RosterEntry entry = roster.getEntry(clientID);
			try {
				roster.removeEntry(entry);
			} catch (XMPPException e) {
				throw new ProtocolException(e.toString());
			}
		} else {
			RosterGroup group = roster.getGroup(groupName);
			RosterEntry entry = group.getEntry(clientID);
			try {
				group.removeEntry(entry);
			} catch (XMPPException e) {
				throw new ProtocolException(e.toString());
			}
		}
	}

	@Override
	public void deleteEntry(String clientID) throws ProtocolException {
		Roster roster = conn.getRoster();
		RosterEntry entry = roster.getEntry(clientID);
		try {
			roster.removeEntry(entry);
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
	}

	@Override
	public void addEntry(String clientID, String alias, String[] groups)
			throws ProtocolException {
		Roster roster = conn.getRoster();
		try {
			roster.createEntry(clientID, alias, groups);
		} catch (XMPPException e) {
			throw new ProtocolException(e.toString());
		}
	}

	@Override
	public void sendSubscriptionRequest(String clientID) {
		Presence presence = new Presence(Presence.Type.subscribe);
		presence.setTo(clientID);
		// presence.setFrom(this.userID);
		conn.sendPacket(presence);
	}

	@Override
	public void sendAuthenticatedReply(String clientID) {
		Presence presence = new Presence(Presence.Type.subscribed);
		presence.setTo(clientID);
		conn.sendPacket(presence);
	}

	@Override
	public void sendDeniedReply(String clientID) {
		Presence presence = new Presence(Presence.Type.unsubscribed);
		presence.setTo(clientID);
		conn.sendPacket(presence);
	}

	@Override
	public void block(String clientID) {
		System.out.println("in block");
		blockList.add(clientID);
		// Create a new presence. Pass in false to indicate we're unavailable.
		Presence presence = new Presence(Presence.Type.unavailable);
		// presence.setStatus("Gone fishing");
		// Send the packet (assume we have a XMPPConnection instance called
		// "con").
		conn.sendPacket(presence);

	}

	@Override
	public void cancelBlock(String clientID) {
		blockList.remove(clientID);
		conn.sendPacket(presence);
	}
	
	@Override	
	public boolean isBlocked(String clientID) {
		if (blockList.contains(clientID)) {
			return true;
		}
		return false;
	}

	/**
	 * connect and login in another thread
	 */
	public void run() {
		try {
			connect();
		} catch (ProtocolException e) {
			eventHandler.connectFailed();
		}
		try {
			login();
		} catch (ProtocolException e) {
			eventHandler.loginFailed();
		}
	}

	public void entriesAdded(Collection<String> addrs) {
		eventHandler.entriesAdded();
	}

	public void entriesDeleted(Collection<String> arg0) {
		System.out.println("deleted : ");
		for (Iterator<String> i = arg0.iterator(); i.hasNext();) {
			String s = i.next();
			System.out.println(s);
		}
	}

	public void entriesUpdated(Collection<String> arg0) {
		System.out.println("updated : ");
		for (Iterator<String> i = arg0.iterator(); i.hasNext();) {
			String s = i.next();
			RosterEntry entry = conn.getRoster().getEntry(s);
			System.out.println(s + " new entry : " + entry.getStatus() + ", "
					+ entry.getType());
		}
	}

	/**
	 * Called when presence of one of your buddies is changed. EventHandler will
	 * be called if it is set.
	 * 
	 * The from id is something like hammerol@gmail.com
	 */
	public void presenceChanged(Presence presence) {
		String from = presence.getFrom().split("/")[0];

		if (this.DEBUG_ENABLED) {
			System.out.println("presenceChanged: " + presence.getFrom() + ", "
					+ presence);
		}

		Status status = null;

		if (presence.getType() == Presence.Type.available) {
			if (presence.getMode() == Presence.Mode.available) {
				status = Status.AVAILABLE;
			} else if (presence.getMode() == Presence.Mode.away) {
				status = Status.AWAY;
			} else if (presence.getMode() == Presence.Mode.chat) {
				status = Status.CHAT;
			} else if (presence.getMode() == Presence.Mode.dnd) {
				status = Status.BUSY;
			} else if (presence.getMode() == Presence.Mode.xa) {
				status = Status.EXTENDED_AWAY;
			} else {
				status = Status.AVAILABLE;
			}
		} else if (presence.getType() == Presence.Type.unavailable) {
			status = Status.UNAVAILABLE;
		}

		if (eventHandler != null) {
			eventHandler.presenceChanged(new UserPresence(from, status));
		}

	}

	@Override
	public void setPresence(Status status) {

		System.out.println("setting presence");
		if (status == Status.AVAILABLE) {
			presence.setType(Presence.Type.available);
			presence.setMode(Presence.Mode.available);
		} else if (status == Status.AWAY) {
			presence.setType(Presence.Type.available);
			presence.setMode(Presence.Mode.away);
		} else if (status == Status.BUSY) {
			presence.setType(Presence.Type.available);
			presence.setMode(Presence.Mode.dnd);
		} else if (status == Status.CHAT) {
			presence.setType(Presence.Type.available);
			presence.setMode(Presence.Mode.chat);
		} else if (status == Status.EXTENDED_AWAY) {
			presence.setType(Presence.Type.available);
			presence.setMode(Presence.Mode.xa);
		} else if (status == Status.UNAVAILABLE) {
			presence.setType(Presence.Type.unavailable);
		}

		conn.sendPacket(presence);
	}

}
